package org.jwebuilder.parsers;

import static org.jwebuilder.util.Constant.BLANK;
import static org.jwebuilder.util.Constant.EMPTY_STRING;
import static org.jwebuilder.util.Constant.LINE_SEPARATOR;
import static org.jwebuilder.util.Constant.SLASH;
import static org.jwebuilder.util.Constant.XML_INDENT;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.servlet.ServletException;
import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLStreamException;

import nu.xom.Attribute;
import nu.xom.Builder;
import nu.xom.Document;
import nu.xom.Element;
import nu.xom.Elements;
import nu.xom.Node;
import nu.xom.Nodes;
import nu.xom.ParsingException;
import nu.xom.Serializer;
import nu.xom.ValidityException;
import nu.xom.XPathContext;
import nux.xom.pool.BuilderPool;
import nux.xom.pool.PoolConfig;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.jcs.JCS;
import org.apache.jcs.access.exception.CacheException;
import org.jwebuilder.GuiceContext;
import org.jwebuilder.event.EventResult;
import org.jwebuilder.event.EventUtils;
import org.jwebuilder.jcs.JcsRegion;
import org.jwebuilder.xom.XmlDocument;
import org.jwebuilder.xom.XmlPageTemplateBuilderFactory;
import org.mvel.TemplateInterpreter;

/**
 * XML 模板解析器
 * @author Xu Jin
 */
public final class XmlPageParser extends TemplateParser
{
	private static final String fileEncoding = "UTF-8";
	public static final String jwbNamespaceURI = "http://www.jwebuilder.org/schema/jwebuilder_template.xsd";
	private static final String xhtmlNamespaceURI = "http://www.w3.org/1999/xhtml";
	public static final String XML_PAGE_ID_PREFIX = "XmlPage:";
	public static final String XHTML_NAMESPACE_PREFIX = "xhtml";
	public static final String JWB_NAMESPACE_PREFIX = "jwb";
	private static final BuilderPool builderPool;
	private static final XPathContext xpathContext;
	static
	{
		builderPool = new BuilderPool(new PoolConfig(), new XmlPageTemplateBuilderFactory());
		xpathContext = new XPathContext(XHTML_NAMESPACE_PREFIX, xhtmlNamespaceURI);
		xpathContext.addNamespace("jwb", jwbNamespaceURI);
	}

	public static Builder getXmlBuilder()
	{
		return builderPool.getBuilder(false);
	}

	public static XPathContext getXpathContext()
	{
		return xpathContext;
	}

	/**
	 * 解析模板
	 * @param template 模板字符串
	 * @param contextObj 上下文对象
	 * @param vars 外部变量
	 * @param templateEncoding 模板文件编码
	 * @return
	 */
	public String parse(String template, Object contextObj, Map<String, Object> vars, String templateEncoding)
	{
		try
		{
			Object page = enableTemplateCache ? getXmlPageObjFromCache(pageId) : null;
			Document document = null;
			if(page == null)
			{
				document = builderPool.getBuilder(false).build(template, null);
			}
			else
			{
				document = ((XmlDocument) page).getDocument();
			}
			return parse(document, vars, templateEncoding, (page == null));
		}
		catch (Exception e)
		{
			throw new RuntimeException(e);
		}
	}

	/**
	 * 解析模板
	 * @param templateFile 模板文件
	 * @param contextObj 上下文对象
	 * @param vars 外部变量
	 * @param templateEncoding 模板文件编码
	 * @return
	 * @throws IOException
	 */
	public String parse(File templateFile, Object contextObj, Map<String, Object> vars, String templateEncoding) throws IOException
	{
		return parse(FileUtils.readFileToString(templateFile, templateEncoding), contextObj, vars, templateEncoding);
	}
	
	/**
	 * 
	 * @param document
	 * @param vars
	 * @param parseTemplate 是否需要解析模板（如果没有在缓存中找到相应的模板，则需要解析）
	 * @return
	 * @throws ClassNotFoundException 
	 * @throws InvocationTargetException 
	 * @throws IllegalAccessException 
	 * @throws IOException 
	 * @throws ParsingException 
	 * @throws ValidityException 
	 * @throws CacheException 
	 * @throws FactoryConfigurationError 
	 * @throws XMLStreamException 
	 * @throws NoSuchMethodException 
	 * @throws ServletException 
	 */
	@SuppressWarnings("unchecked")
	private String parse(Document document, Map<String, Object> vars, String templateEncoding, boolean parseTemplate) throws ClassNotFoundException, IllegalAccessException, InvocationTargetException, ValidityException, ParsingException, IOException, CacheException, XMLStreamException, FactoryConfigurationError, NoSuchMethodException, ServletException
	{
		Attribute attribute_PageClass = document.getRootElement().getAttribute("pageClass", jwbNamespaceURI);

		if(parseTemplate)
		{
			document = parseMasterPage(document, false, templateEncoding);
			document = parseIncludePage(document, false, templateEncoding);
			document = parseMasterPage(document, false, templateEncoding);
			// TODO 解析组件（如果组件为资源文件则认为组件所使用的母版页和包含页面也为资源文件）
			document = parseImport(document);

			// 为解析后的页面模板附上相应的 PageClass 属性（解析母版页后可能会造成 PageClass 属性丢失）
			if(attribute_PageClass != null)
			{
				attribute_PageClass.detach();
				document.getRootElement().addAttribute(attribute_PageClass);
			}

			if(enableTemplateCache)
			{
				// 缓存已解析的页面对象
				JCS.getInstance(JcsRegion.XOM).put(pageId, new XmlDocument(pageId, templateEncoding, document));
			}
		}

		Document documentCopy = new Document(document);

		Map parameters = new HashMap();
		Object cxtObj = null;
		if(attribute_PageClass != null)
		{
			cxtObj= GuiceContext.getInstance().getInjector().getInstance(Class.forName(attribute_PageClass.getValue()));

			// 给 PageClass 实例的属性赋值
			BeanUtils.populate(cxtObj, vars);

			Object jWebuilder_EventHandler = vars.get(EventUtils.EVENT_HANDLER_PARAMETER_NAME);
			if(jWebuilder_EventHandler != null)
			{
				String jWebuilderEventHandler = jWebuilder_EventHandler.toString();
				Object result = null;
				if(jWebuilderEventHandler.startsWith(EventUtils.EVENT_HANDLER_PARAMETER_VALUE_PREFIX))
				{
					// 必须指定 Annotation 的完全限定名
					result = EventUtils.fireEvent(Class.forName(jWebuilderEventHandler.substring(EventUtils.EVENT_HANDLER_PARAMETER_VALUE_PREFIX.length())).asSubclass(Annotation.class), cxtObj);
				}
				else
				{
					// 直接调用指定的事件处理器方法
					result = EventUtils.invokeEventHandler(jWebuilderEventHandler, cxtObj);
				}
				if(result != null && result.getClass() == EventResult.class)
				{
					EventResult eventResult = (EventResult) result;
					return eventResult.process(request, response);
				}
			}

			if(attribute_PageClass != null)
			{
				documentCopy.getRootElement().removeAttribute(documentCopy.getRootElement().getAttribute(attribute_PageClass.getLocalName(), jwbNamespaceURI));
			}

			parameters = PropertyUtils.describe(cxtObj);
		}

		documentCopy.getRootElement().removeNamespaceDeclaration(JWB_NAMESPACE_PREFIX);

		if(cxtObj != null) // 触发页面渲染前事件
		{
			Object result = EventUtils.fireBeforeRender(cxtObj);
			if(result != null && result.getClass() == EventResult.class)
			{
				EventResult eventResult = (EventResult) result;
				return eventResult.process(request, response);
			}
		}

		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
		Serializer outputer = new Serializer(byteArrayOutputStream, templateEncoding);
		outputer.setIndent(XML_INDENT);
		outputer.setLineSeparator(LINE_SEPARATOR);
		outputer.write(documentCopy);
		String outputText = byteArrayOutputStream.toString(templateEncoding);
		byteArrayOutputStream.close();

		String propertyName;
		for(Iterator<String> keys = vars.keySet().iterator(); keys.hasNext();)
		{
			propertyName = keys.next();
			if(!parameters.containsKey(propertyName))
			{
				parameters.put(propertyName, vars.get(propertyName));
			}
		}

		String pageText = TemplateInterpreter.parse(outputText, parameters, new HashMap<String, Object>(1)).toString();
		
		if(cxtObj != null) // 触发页面渲染后事件
		{
			Object result = EventUtils.fireAfterRender(cxtObj);
			if(result != null && result.getClass() == EventResult.class)
			{
				EventResult eventResult = (EventResult) result;
				return eventResult.process(request, response);
			}
		}
		
		return pageText;
	}
	
	private Object getXmlPageObjFromCache(String id) throws CacheException
	{
		return JCS.getInstance(JcsRegion.XOM).get(id);
	}

	/**
	 * 解析母版页
	 * @param document
	 * @param isResource
	 * @param templateEncoding
	 * @return
	 * @throws ValidityException
	 * @throws ParsingException
	 * @throws IOException
	 */
	private Document parseMasterPage(Document document, boolean isResource, String templateEncoding) throws ValidityException, ParsingException, IOException
	{
		Nodes nodes = document.query("//" + JWB_NAMESPACE_PREFIX + ":contentPlaceHolder", xpathContext);
		Element contentPlaceHolder;
		for(int i = nodes.size() - 1; i >= 0; i--)
		{
			Node child;
			contentPlaceHolder = (Element) nodes.get(i);
			int position = contentPlaceHolder.getParent().indexOf(contentPlaceHolder);
			for(int index = contentPlaceHolder.getChildCount() - 1; index >= 0; index--)
			{
				child = contentPlaceHolder.getChild(index);
				child.detach();
				contentPlaceHolder.getParent().insertChild(child, position);
			}
			contentPlaceHolder.getParent().removeChild(contentPlaceHolder);
		}

		Attribute attribute_MasterPage = document.getRootElement().getAttribute("masterPage", jwbNamespaceURI);
		if(attribute_MasterPage != null)
		{
			Document masterPage;
			if(isResource)
			{
				masterPage = builderPool.getBuilder(false).build(IOUtils.toString(getClass().getResourceAsStream(attribute_MasterPage.getValue()), fileEncoding));
			}
			else
			{
				masterPage = builderPool.getBuilder(false).build(FileUtils.readFileToString(new File(application.getRealPath(attribute_MasterPage.getValue())), templateEncoding), null);
			}
			Nodes contentNodes = document.query("//" + JWB_NAMESPACE_PREFIX + ":content", xpathContext);
			String contentPlaceHolderId;
			Element content;
			for(int i = contentNodes.size() - 1; i >= 0; i--)
			{
				content = (Element) contentNodes.get(i);
				contentPlaceHolderId = content.getAttributeValue("contentPlaceHolderId");
				contentPlaceHolder = (Element) masterPage.query(MessageFormat.format("//" + JWB_NAMESPACE_PREFIX + ":contentPlaceHolder[@id=''{0}'']", contentPlaceHolderId), xpathContext).get(0);
				Node child;
				int position = contentPlaceHolder.getParent().indexOf(contentPlaceHolder);
				for(int index = content.getChildCount() - 1; index >= 0; index--)
				{
					child = content.getChild(index);
					child.detach();
					contentPlaceHolder.getParent().insertChild(child, position);
				}
				contentPlaceHolder.getParent().removeChild(contentPlaceHolder);
			}
			document = parseMasterPage(masterPage, isResource, templateEncoding);
		}

		return document;
	}
	
	/**
	 * 解析静态包含页面
	 * @param document
	 * @param isResource
	 * @param templateEncoding
	 * @return
	 * @throws ValidityException
	 * @throws ParsingException
	 * @throws IOException
	 */
	private Document parseIncludePage(Document document, boolean isResource, String templateEncoding) throws ValidityException, ParsingException, IOException
	{
		Nodes nodes = document.query("//" + JWB_NAMESPACE_PREFIX + ":include", xpathContext);
		Element include;
		Document includePage;
		Element body;
		for(int i = nodes.size() - 1; i >= 0; i--)
		{
			Node child;
			include = (Element) nodes.get(i);
			int position = include.getParent().indexOf(include);
			if(isResource)
			{
				includePage = builderPool.getBuilder(false).build(IOUtils.toString(getClass().getResourceAsStream(include.getAttributeValue("file")), fileEncoding));
			}
			else
			{
				includePage = builderPool.getBuilder(false).build(FileUtils.readFileToString(new File(application.getRealPath(include.getAttributeValue("file"))), templateEncoding), null);
			}
			body = includePage.getRootElement().getFirstChildElement("body", xhtmlNamespaceURI);
			for(int index = body.getChildCount() - 1; index >= 0; index--)
			{
				child = body.getChild(index);
				child.detach();
				include.getParent().insertChild(child, position);
			}
			include.getParent().removeChild(include);
		}

		if(nodes.size() > 0)
		{
			document = parseIncludePage(document, isResource, templateEncoding);
		}

		return document;
	}

	/**
	 * 解析导入（JavaScript、CSS）
	 * @param document
	 * @return
	 */
	private Document parseImport(Document document)
	{
		Element head = document.getRootElement().getFirstChildElement("head", xhtmlNamespaceURI);
		if(head == null)
		{
			document.getRootElement().insertChild(head = new Element("head", xhtmlNamespaceURI), 0);
		}

		int position = head.getChildCount();
		boolean exist = false;
		ArrayList<String> files = new ArrayList<String>();

		// 导入 CSS
		Elements links = head.getChildElements("link", xhtmlNamespaceURI);
		Nodes importCSSs = document.query("//" + JWB_NAMESPACE_PREFIX + ":importCSS", xpathContext);
		Element importCSS;
		Element link;
		for (int index = 0; index < links.size(); index++)
		{
			link = links.get(index);
			link.addAttribute(new Attribute("href", getFullPath(link.getAttributeValue("href"))));
		}
		for (int i = importCSSs.size() - 1; i >=0 ; i--)
		{
			importCSS = (Element) importCSSs.get(i);
			for (int index = 0; index < links.size(); index++)
			{
				link = links.get(index);
				if(link.getAttributeValue("href").equals(getFullPath(importCSS.getAttributeValue("file"))))
				{
					exist = true;
					break;
				}
			}
			if(!exist)
			{
				for (String file : files)
				{
					if(file.equals(importCSS.getAttributeValue("file")))
					{
						exist = true;
						break;
					}
				}
			}

			if(exist)
			{
				exist = false;
			}
			else
			{
				Element newLink = new Element("link", xhtmlNamespaceURI);
				newLink.addAttribute(new Attribute("rel", "stylesheet"));
				newLink.addAttribute(new Attribute("type", "text/css"));
				newLink.addAttribute(new Attribute("href", getFullPath(importCSS.getAttributeValue("file"))));
				String charset = importCSS.getAttributeValue("encoding");
				if(charset != null)
				{
					newLink.addAttribute(new Attribute("charset", charset));
				}
				head.insertChild(newLink, position);
			}
			files.add(importCSS.getAttributeValue("file"));
			importCSS.getParent().removeChild(importCSS);
		}

		files.clear();
		position = head.getChildCount();

		// 导入 JavaScript
		Elements scripts = head.getChildElements("script", xhtmlNamespaceURI);
		Nodes importJavaScripts = document.query("//" + JWB_NAMESPACE_PREFIX + ":importJavaScript", xpathContext);
		Element importJavaScript;
		Element script;
		for (int index = 0; index < scripts.size(); index++)
		{
			script = scripts.get(index);
			if(script.getAttributeValue("src") != null)
			{
				script.addAttribute(new Attribute("src", getFullPath(script.getAttributeValue("src"))));
				if(script.getChildCount() == 0) // Hack for "script" tag
				{
					script.appendChild(BLANK);
				}
			}
		}
		for (int i = importJavaScripts.size() - 1; i >=0 ; i--)
		{
			importJavaScript = (Element) importJavaScripts.get(i);
			for (int index = 0; index < scripts.size(); index++)
			{
				script = scripts.get(index);
				if(script.getAttributeValue("src") != null && script.getAttributeValue("src").equals(getFullPath(importJavaScript.getAttributeValue("file"))))
				{
					exist = true;
					break;
				}
			}
			if(!exist)
			{
				for (String file : files)
				{
					if(file.equals(importJavaScript.getAttributeValue("file")))
					{
						exist = true;
						break;
					}
				}
			}

			if(exist)
			{
				exist = false;
			}
			else
			{
				Element newScript = new Element("script", xhtmlNamespaceURI);
				newScript.addAttribute(new Attribute("type", "text/javascript"));
				newScript.addAttribute(new Attribute("src", getFullPath(importJavaScript.getAttributeValue("file"))));
				String charset = importJavaScript.getAttributeValue("encoding");
				if(charset != null)
				{
					newScript.addAttribute(new Attribute("charset", charset));
				}
				newScript.appendChild(BLANK);
				head.insertChild(newScript, position);
			}
			files.add(importJavaScript.getAttributeValue("file"));
			importJavaScript.getParent().removeChild(importJavaScript);
		}

		files.clear();

		return document;
	}

	private String getFullPath(String part)
	{
		final String contextPath = request.getContextPath();
		if(part.startsWith(SLASH) && !EMPTY_STRING.equals(contextPath))
		{
			part = contextPath + part;
		}
		return part;
	}
}